﻿// Copyright (c) 2012-2013 fancidev
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Util.Media;

namespace Util.Forms
{
    [DefaultEvent("ActiveItemChanged")]
    public partial class VirtualFolderListView : UserControl
    {
        /// <summary>
        /// Creates a VirtualFolderListView.
        /// </summary>
        public VirtualFolderListView()
        {
            InitializeComponent();
        }

        /// <summary> 
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be 
        /// disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                ResetListView();
                if (components != null)
                    components.Dispose();
                if (spinner != null)
                    spinner.Dispose();
                if (schedulerExtractIcon != null)
                    schedulerExtractIcon.Dispose();
                if (ctsEnumItem != null)
                    ctsEnumItem.Dispose();
                if (ctsExtractIcon != null)
                    ctsExtractIcon.Dispose();
                if (animator != null)
                    animator.Dispose();
            }
            base.Dispose(disposing);
        }

#if DEBUG && false
        private static void DebugPrint(string format, params object[] args)
        {
            string msg = string.Format(format, args);
            if (msg == _previousDebugPrintMessage)
            {
                System.Diagnostics.Debug.Write(".");
            }
            else
            {
                System.Diagnostics.Debug.WriteLine("");
                System.Diagnostics.Debug.Write(
                    "[" + DateTime.Now.ToString("HH:mm:ss.fff") + "] " + msg);
                _previousDebugPrintMessage = msg;
            }
        }
        private static string _previousDebugPrintMessage;
#else
        private static void DebugPrint(string format, params object[] args)
        {
        }
#endif

        /// <summary>
        /// The spinner GIF image. The default spinner is taken from
        /// http://preloaders.net/en/circular
        /// </summary>
        private MultiFrameImage spinner;

        /// <summary>
        /// Controls animation of the thumbnails.
        /// </summary>
        private AnimationManager animator;

        /// <summary>
        /// Initializes the image list.
        /// </summary>
        private void InitializeImageList()
        {
            if (smartImageList1.ImageCount != 0)
                throw new InvalidOperationException("Cannot initialize twice.");

            // Add a dummy image to the image list. This image will be drawn
            // upon in the DrawItem() event handler.
            Bitmap dummy = new Bitmap(1, 1, PixelFormat.Format32bppRgb);
            smartImageList1.AddImage(dummy);

            // Load spinner frames from resource file.
#if true
            using (var memstream = new System.IO.MemoryStream(VirtualFolderResources.Spinner))
            {
                this.spinner = new SwfImage(memstream);
            }
#else
            this.spinner = new GdiImage(VirtualFolderResources.Spinner_Gif);
#endif

            lvFiles.LargeImageList = smartImageList1.NativeImageList;
        }

        /// <summary>
        /// The virtual folder being displayed.
        /// </summary>
        private IVirtualFolder _folder;

        /// <summary>
        /// Gets or sets the virtual folder being displayed by this view.
        /// </summary>
        [Browsable(false)]
        public IVirtualFolder Folder
        {
            get { return _folder; }
            set
            {
                if (_folder != value)
                {
                    ResetListView();
                    _folder = value;
                    PopulateListView();
                }
            }
        }

        /// <summary>
        /// Gets the first selected item, or the selected item that has focus.
        /// If no item is selected, returns null. This property must be accessed
        /// from the UI thread.
        /// </summary>
        public IVirtualItem ActiveItem
        {
            get
            {
                if (lvFiles.SelectedIndices.Count == 0)
                    return null;
                int k = lvFiles.SelectedIndices[0];
                return (k < entries.Count) ? entries[k].VirtualItem : null;
            }
        }

        /// <summary>
        /// Occurs when the ActiveItem property changed.
        /// </summary>
        public event EventHandler ActiveItemChanged;

        /// <summary>
        /// Gets or sets the thumbnail size.
        /// </summary>
        [Description("Size of a thumbnail in thumbnail view mode.")]
        public Size ThumbnailSize
        {
            get { return smartImageList1.ImageSize; }
            set { smartImageList1.ImageSize = value; }
        }

        //public Size LargeIconSize; // large icon, tile
        //public Size SmallIconSize; // small icon, list

        // Note: an icon is never framed.

        /// <summary>
        /// Gets or sets the color of a thumbnail's frame.
        /// </summary>
        [Description("Width in pixels of a thumbnail's frame; 0 = no frame.")]
        [DefaultValue(typeof(Color), "0, 0, 0")]
        public Color ThumbnailFrameColor
        {
            get { return smartImageList1.FrameColor; }
            set { smartImageList1.FrameColor = value; }
        }

        /// <summary>
        /// Gets or sets the width of a thumbnail's frame. A value of zero
        /// indicates no frame.
        /// </summary>
        [Description("Width in pixels of a thumbnail's frame; 0 = no frame.")]
        [DefaultValue(0)]
        public int ThumbnailFrameWidth
        {
            get { return smartImageList1.FrameWidth; }
            set { smartImageList1.FrameWidth = value; }
        }

        /// <summary>
        /// Gets or sets the margin outside a thumbnail's frame.
        /// </summary>
        [Description("Margin in pixels outside a thumbnail's frame.")]
        [DefaultValue(0)]
        public int ThumbnailFrameMargin
        {
            get { return smartImageList1.FrameMargin; }
            set { smartImageList1.FrameMargin = value; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether multiple items can be selected.
        /// </summary>
        [Description("Allows multiple items to be selected.")]
        [DefaultValue(true)]
        public bool MultiSelect
        {
            get { return lvFiles.MultiSelect; }
            set { lvFiles.MultiSelect = value; }
        }

        /// <summary>
        /// The internal view model that sits in between the data represented
        /// by IVirtualItem and the view represented by ListView.
        /// </summary>
        private List<ListEntry> entries;

        /// <summary>
        /// Custom scheduler for icon extraction tasks. This scheduler has
        /// the following features compared to the default scheduler:
        /// 1. It allows to specify the number of worker threads, and 
        ///    guarantees to start a worker thread immediately.
        /// 2. Tasks are scheduled in LIFO order, which suits our need.
        /// 3. It supports task priority adjustment, so we don't need to
        ///    create a series of Task objects to do the same thing just to
        ///    make them execute sooner.
        /// </summary>
        private ManualTaskScheduler schedulerExtractIcon;

        /// <summary>
        /// Cancellation token source for icon extraction tasks. This member
        /// should only be accessed from the UI thread.
        /// </summary>
        private CancellationTokenSource ctsExtractIcon;

        /// <summary>
        /// Cancellation token source for children enumeration tasks.
        /// </summary>
        private CancellationTokenSource ctsEnumItem;

        // TODO: observe ctsEnumItem.

        /// <summary>
        /// Reset the ListView and stop all background tasks. This function
        /// must be called from the UI thread.
        /// </summary>
        private void ResetListView()
        {
            // Clear the list view.
            lvFiles.SelectedIndices.Clear();
            lvFiles.VirtualListSize = 0;

            // Ideally we should scroll the list view to the top, but I can't
            // get this to work easily.
            //lvFiles.Scroll(0, -10000);
            // TODO: maybe we can Invalidate it first? or add a dummy item?
            // in order to reset the scroll bar...

            // Cancel existing tasks.
            // TODO: should be dispose the cancellation token sources?
            if (ctsExtractIcon != null)
            {
                ctsExtractIcon.Cancel();
                ctsExtractIcon = null;
            }
            if (ctsEnumItem != null)
            {
                ctsEnumItem.Cancel();
                ctsEnumItem = null;
            }

            // Dispose the custom task scheduler.
            if (schedulerExtractIcon != null)
            {
                schedulerExtractIcon.Dispose();
                schedulerExtractIcon = null;
            }

            // Dispose the animation controller.
            if (animator != null)
            {
                animator.Dispose();
                animator = null;
            }

#if false
            // Unsubscribe to the FolderChanged event of the previous folder.
            if (_folder is INotifyFolderChanged)
            {
                (_folder as INotifyFolderChanged).FolderChanged -= OnFolderChanged;
            }
#endif

            // Clear the view model.
            if (entries != null)
            {
                foreach (ListEntry ent in entries)
                {
                    if (ent.Icon != null)
                        ent.Icon.Dispose();
                }
                entries = null;
            }
        }

        /// <summary>
        /// Populates the list view with children from the underlying virtual
        /// folder. After the function returns, no item will be selected in
        /// the ListView.
        /// </summary>
        private void PopulateListView()
        {
            // Create a custom scheduler for ExtractIcon tasks.
            schedulerExtractIcon = new ManualTaskScheduler();

            // Create a cancellation token source for icon extraction.
            ctsExtractIcon = new CancellationTokenSource();

            // Create an animation controller.
            animator = new AnimationManager();
            animator.FrameExpired += delegate(object sender, FrameExpiredEventArgs e)
            {
                // If we only want to Invalidate the control, we don't need
                // to marshal the call. If we also want to Update the control,
                // we need to marshal the call.
#if true
                lvFiles.Invalidate();
#else
                // TODO: sometimes we get an error...
                if (this.IsHandleCreated)
                {
                    this.BeginInvoke(new Action(() =>
                    {
#if false
                        if (e.Image.Name != null && e.Image.Name.StartsWith("fenshu."))
                        {
                            System.Diagnostics.Debug.WriteLine(
                                "[{0}] >>> FrameExpired({1}, {2})",
                                DateTime.Now.TimeOfDay, e.Image.Name, e.Image.FrameIndex);
                        }
#endif
                        lvFiles.Invalidate();
                        //lvFiles.Update();
                    }));
                }
#endif
            };
            animator.Resolution = 50;
            animator.Add(spinner);

            entries = new List<ListEntry>();
#if false
            // Create a background thread to enumerate the items in this 
            // folder, store them in an internal view model, and notifies the
            // UI for update. We must create a thread manually rather than
            // relying on Task.Factory.StartNew() because the latter may not
            // actually start the task immediately, which is what we want.
            Thread thread2 = new Thread(EnumerateItemsRoutine);
            thread2.IsBackground = true;
            thread2.Start();
#else
            ctsEnumItem = new CancellationTokenSource();
            Task.Factory.StartNew(
                EnumerateItemsRoutine, // routine
                ctsEnumItem.Token,     // state
                ctsEnumItem.Token,     // cancellation token
                TaskCreationOptions.LongRunning, // run immediately
                TaskScheduler.Default);
#endif

#if false
            // Subscribe to the FolderChanged event if supported.
            if (_folder is INotifyFolderChanged)
            {
                (_folder as INotifyFolderChanged).FolderChanged += OnFolderChanged;
            }
#endif
        }

#if false
        private void OnFolderChanged(object sender, FolderChangedEventArgs e)
        {
            if (sender != _folder)
                return;

            // TODO: Marshal to the UI thread.

            IVirtualItem vItem = e.Item;
            if (e.ChangeType == FolderChangeType.ItemIconChanged)
            {
                // Find the list entry containing this item.
                int index = -1;
                for (int i = 0; i < entries.Count; i++)
                {
                    if (entries[i].VirtualItem == vItem)
                    {
                        index = i;
                        break;
                    }
                }
                if (index < 0)
                    return;

                // Schedule the icon to be loaded again.
                ExtractIconAsync(index, true);
            }
        }
#endif

        /// <summary>
        /// The worker routine to enumerate items contained in the virtual
        /// folder. This routine is executed in a background thread. Note 
        /// that accesses to instance members may not be the same as when
        /// the task was initiated.
        /// </summary>
        private void EnumerateItemsRoutine(object state)
        {
            // TODO: can we turn the enumeration into individual tasks?
            CancellationToken ct = (CancellationToken)state;

            // TODO: observe cancellation token.
            // avoid race condition (need to pass cancellation token and folder)
            IVirtualFolder folder = _folder;
            if (folder == null)
                return;

            if (ct.IsCancellationRequested)
                return;

            var children = folder.EnumerateItems(VirtualItemTypes.NonFolder);
            if (children == null)
                return;

            foreach (IVirtualItem vItem in children)
            {
                if (ct.IsCancellationRequested)
                    return;

                // TODO: can we avoid invocation exception? Maybe use a 
                // synchronization context i/o the control handle?
                try
                {
                    // Make a copy of the variable for closure.
                    IVirtualItem t = vItem;

                    // We must use Invoke, otherwise there will be
                    // too many messages for the UI to process and
                    // it will get stuck.
                    //this.BeginInvoke(new Action(() => OnItemAdded(t)));
                    this.Invoke(new Action(() => OnItemAdded(t, ct)));
                }
                catch (InvalidOperationException) // maybe form disposed
                {
                    break;
                }
            }
        }

        /// <summary>
        /// Occurs when a new item is discovered. This must be called from the
        /// UI thread.
        /// </summary>
        // TODO: maybe in the future we do this in a handler to
        // INotifyFolderChange...
        private void OnItemAdded(IVirtualItem vItem, CancellationToken ct)
        {
            // We must add this, otherwise we may add wrong items.
            if (ct.IsCancellationRequested)
                return;

            entries.Add(new ListEntry
            {
                VirtualItem = vItem,
                Icon = null,
                CachedItem = null,
                ExtractIconTask = null
            });

            // Setting VirtualListSize triggers a repaint of the control.
            // If called successively, this can cause the UI to stuck.
            // A way to get around this is to update the UI only every
            // 100 ms.
            
            // Strangely, if we add the following line, the icons won't render
            // properly. Weird!!!
            //if (entries.Count % 10 == 9)
            lvFiles.VirtualListSize = entries.Count;
        }

        private void VirtualFolderListView_Load(object sender, EventArgs e)
        {
            lvFiles.SetWindowTheme("explorer");
            lvFiles.HideFocus();

            // Initialize the image list for thumbnails.
            InitializeImageList();
        }

        private void lvFiles_DrawItem(object sender, DrawListViewItemEventArgs e)
        {
            // TODO: still, sometimes, the image list is disposed but 
            // this event still fires. This can happen when the form
            // is closing but this item is still valid.
            //System.Diagnostics.Debug.WriteLine("");
            DebugPrint("DrawItem({0})", e.ItemIndex);

            e.DrawDefault = true;
            if (e.ItemIndex < 0 || e.ItemIndex >= entries.Count)
                return;
            ListEntry ent = entries[e.ItemIndex];

            // If we have loaded the thumbnail for this item, select it into
            // the active image for display. Otherwise, select the current
            // spinner frame to display.
            if (ent.Icon != null)
            {
                if (ent.Icon.FrameCount == 1)
                    smartImageList1.SetImage(0, ent.Icon.Frame);
                else
                    smartImageList1.SetImage(0, animator.GetCurrentFrame(ent.Icon, true));
            }
            else
            {
                smartImageList1.SetImage(0, animator.GetCurrentFrame(spinner, true));
            }
        }

        private void lvFiles_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
        {
            DebugPrint("RetrieveVirtualItem({0})", e.ItemIndex);

            int i = e.ItemIndex;
            if (i < 0 || i >= entries.Count)
                return;

            ListEntry ent = entries[i];
            ListViewItem item = ent.CachedItem;
            if (item == null)
            {
                item = new ListViewItem();
                item.Text = ent.VirtualItem.DisplayName;
                item.ImageIndex = 0;
                ent.CachedItem = item;
#if DEBUG
                //System.Diagnostics.Debug.WriteLine("Retrieving uncached item "
                //    + e.ItemIndex);
#endif
            }

            // TBD: This or the one in CacheVirtualItem should be removed.
            //if (ent.Icon == null)
            //    ExtractIconAsync(i);

            // Add properties of the vItem as subitems of the ListViewItem.
            //item.SubItems.Add(entry.Size.ToString("#,#"));
            e.Item = item;
        }

        /// <summary>
        /// Schedules the icon of a ListEntry to be extracted as soon as 
        /// possible. This function must be called from the UI thread.
        /// </summary>
        /// <param name="index">Index of the ListEntry whose icon is to be
        /// extracted.</param>
        private void ExtractIconAsync(int index)
        {
            // Create local variables for closure.
            ListEntry ent = entries[index];
            ExtractIconType iconType = ExtractIconType.Thumbnail;
            Size iconSize = this.ThumbnailSize;
            Task<MultiFrameImage> task = null;

            // TODO: Maybe we should use the Invoke() logic instead of 
            // BeginInvoke() logic to draw the icons more slowly, and
            // reduce the speed we post messages to the UI.

            // TODO: If the item doesn't implement the IExtractIcon interface,
            // just use the default icon.
            if (!(ent.VirtualItem is IExtractIcon))
                return;

            // If there is already a task associated with this entry,
            // reschedule the task to be executed as soon as possible.
            if (ent.ExtractIconTask != null)
            {
                schedulerExtractIcon.Prioritize(ent.ExtractIconTask);
                return;
            }

            // Create a task to extract this icon.
            task = new Task<MultiFrameImage>(() =>
            {
                //Thread.Sleep(5000);
                IExtractIcon vIcon = ent.VirtualItem as IExtractIcon;
                object icon = vIcon.ExtractIcon(iconType, iconSize);
                if (icon == null)
                {
                    icon = Shell.GetExtensionIcon(ent.VirtualItem.Name);
                }
                if (icon is Image)
                    return new GdiImage(icon as Image);
                if (icon is MultiFrameImage)
                    return icon as MultiFrameImage;
                using (icon as IDisposable) { }
                return null;
            }, ctsExtractIcon.Token);

            // Update the UI (from the UI thread) after the icon is extracted.
            // either successfully or with an error.
            task.ContinueWith((Task<MultiFrameImage> t) =>
            {
                // TODO: if result is null, we must assign something to
                // ListEntry.Icon to prevent a task from being launched
                // again.
                if (t.Result == null)
                    return;

                // Check whether the icon extracted is still relevant. For
                // For example, the user may have switched the View to Details
                // mode, or even the selected VirtualFolder may have changed.
                int k = index;
                if (k < entries.Count && entries[k].ExtractIconTask == task)
                {
                    // Set the entry's icon, and reset task.
                    entries[k].Icon = task.Result;
                    entries[k].ExtractIconTask = null;

                    // Add the icon to animation control.
                    if (entries[k].Icon.FrameCount > 1)
                    {
                        animator.Add(entries[k].Icon);
                    }

                    // Invalidate the ListViewItem whose icon has just been
                    // extracted.
                    // TODO: Invalidate the item only if it's visible.
                    if (k < lvFiles.Items.Count)
                    {
                        lvFiles.RedrawItems(k, k, true);
                    }
                }

                // Dispose the previous task object can reduce the number of
                // GCHandles. We yet need to find a way to dispose this task
                // object...
                t.Dispose();
            },
            ctsExtractIcon.Token,
            TaskContinuationOptions.NotOnCanceled,
            TaskScheduler.FromCurrentSynchronizationContext());

            // Start the task.
            ent.ExtractIconTask = task;
            task.Start(schedulerExtractIcon);
        }

        /// <summary>
        /// Handles the CacheVirtualItems event. This event is good indication
        /// that the specified range of items are about to be rendered. So
        /// we load their icons if not already loaded.
        /// </summary>
        private void lvFiles_CacheVirtualItems(object sender, CacheVirtualItemsEventArgs e)
        {
            DebugPrint("CacheVirtualItems({0}, {1})", e.StartIndex, e.EndIndex);

            // Run the loop reversed so that items with smaller index 
            // are loaded first.
            for (int i = Math.Min(e.EndIndex, entries.Count - 1); i >= e.StartIndex; i--)
            {
                ListEntry ent = entries[i];

                // Extract the icon of this entry is not extracted.
                if (ent.Icon == null)
                    ExtractIconAsync(i);

                // Create a ListViewItem for this entry if not yet.
                ListViewItem item = ent.CachedItem;
                if (item == null)
                {
                    item = new ListViewItem();
                    item.Text = ent.VirtualItem.DisplayName;
                    ent.CachedItem = item;
                }
            }
        }

        private void lvFiles_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (this.ActiveItemChanged != null)
                this.ActiveItemChanged(this, null);
        }

        /// <summary>
        /// Gets the loaded thumbnail of the given item. If the thumbnail for
        /// that item is not loaded, returns null. This method must be called
        /// from the UI thread. The returned thumbnail must not be disposed
        /// by the caller, and is valid only until the next UI call.
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public MultiFrameImage GetLoadedThumbnail(IVirtualItem item)
        {
            foreach (var ent in entries)
            {
                if (ent.VirtualItem == item)
                    return ent.Icon;
            }
            return null;
        }

        private void lvFiles_Enter(object sender, EventArgs e)
        {
            lvFiles.HideFocus();
        }
    }

    /// <summary>
    /// Contains UI-related information about a virtual item in the list view.
    /// This serves as the backing view model for the ListView and is used to
    /// construct ListViewItems in a Virtual Mode list view.
    /// </summary>
    class ListEntry
    {
        /// <summary>
        /// The virtual item represented by this entry.
        /// </summary>
        public IVirtualItem VirtualItem;

        /// <summary>
        /// The icon to display for this entry, or null if it has not been
        /// extracted yet.
        /// </summary>
        public MultiFrameImage Icon;

        /// <summary>
        /// The current ExtractIconTask scheduled for this entry.
        /// </summary>
        public Task<MultiFrameImage> ExtractIconTask;

        /// <summary>
        /// The cached list view item associated with this entry.
        /// </summary>
        public ListViewItem CachedItem;
    }
}
